Articles

Jun 15 2025

Value Objects for the Win!

From Scattered Data to Cohesive Design

 

You've seen this method signature before: get_items(from_date, to_date, status, user_id, limit, ...). Each parameter seems reasonable in isolation, but together they form a fragmented representation of what should be cohesive concepts. The from and to dates belong together—they represent a date range with inherent rules and behaviors that get lost when treated as separate primitives.

The Fragmentation Problem

I experienced this pain firsthand working with many systems where date ranges were passed as separate parameters throughout the entire codebase. Every layer—from the controller to the business logic to the data access layer—had to constantly reassemble these fragments to understand what they actually represented.

The validation logic was repeated everywhere. Or in the "best" case, extracted as a utils function hoping the developer would remember to add it. Most of the time ended up applying a defensive approach and repeating the validation every time. Each layer reimplemented the same basic question: "Is this a valid date range?" There was no single source of truth to rely on.

Business rules became impossible to enforce consistently. Some methods assumed the start date came before the end date, others didn't. Some allowed same-day ranges, others rejected them. The lack of cohesion meant that domain knowledge was scattered across dozens of functions, each making its own assumptions about these supposedly "simple" date parameters.

Value Objects: Cohesion Through Encapsulation

Value Objects, a pattern from Domain-Driven Design, solve this by bundling related data with its associated behavior and invariants. Instead of scattered primitives, we create a cohesive unit that encapsulates both the data and the rules governing it.

Creating a class, type, struct, ... whatever is your poison, that enforces validation rules when creating it, automatically removes all the burden from your shoulders. If you handle a DateRange you can be sure it's a valid one, whatever the business rules are.

Now our method signature becomes get_items(date_range, status, user_id, limit, ...). The date_range carries its own validation, behavior, and invariants. Every layer of the application works with the same, consistently valid representation. There's no more reassembly, no scattered validation, no assumptions about what the dates mean—the object itself embodies the domain concept.

The Broader Pattern

This pattern extends beyond date ranges. Money objects prevent currency mixing errors. Email objects ensure valid formatting. Geographic coordinates bundle latitude and longitude with distance calculations and boundary checks. The key insight is recognizing when scattered primitives actually represent a cohesive domain concept.

Value Objects shine when primitives have rules, relationships, or behaviors that matter to your domain. A simple string might be fine for a user's nickname, but an email address has validation rules, formatting concerns, and domain-specific behavior that justify the additional structure. Consider determining if an email belongs to an employee—you could scatter this logic with utility functions like is_employee_email(email_string), but placing it as email.is_employee() keeps the domain logic cohesive with the data.

The immutability and equality semantics come naturally—two DateRanges with the same start and end dates are equivalent, regardless of when or where they were created. Methods like overlaps() or duration() express domain concepts directly rather than forcing readers to decipher date arithmetic scattered throughout the codebase.

Making the Choice

Value Objects aren't a universal solution. They add a layer of structure that may not always pay for itself. The decision comes down to whether the cohesion and expressiveness justify the additional complexity for your specific domain and team.

When primitives start traveling in groups, when validation logic gets duplicated, or when domain concepts get lost in scattered data—that's when Value Objects earn their place in your design toolkit.